perf(moe): two-kernel decode-MoE that beats gather_qmm (#268 step 2b) - #276
Merged
Conversation
Parallelize the fused decode-MoE expert kernel from step 2a so it actually beats the gather_qmm baseline instead of underutilizing the GPU. The 2a kernel was correct but ran one threadgroup per token (one GPU core), making it ~88x slower than gather_qmm. Two changes fix that, validated greedy byte-identical throughout on qwen3-30b-a3b (M1 Ultra): 1. SIMD-cooperative GEMV (one simdgroup per output row, 32 lanes stride the contraction dim + simd_sum) lifts it to 21.6 tok/s (40x over 2a), but still 0.46x of gather_qmm. An occupancy sweep confirmed the kernel is occupancy-bound: a single fused launch pins each expert to one threadgroup because the gate/up -> down dependency runs through the on-chip activation, so it cannot fill more than K cores without recomputing gate/up redundantly. 2. Break that barrier: stage the swiglu activation in global memory across two dispatches (A: gate/up + swiglu -> act_g[K, Dff]; B: down * score -> partial[K, Din], summed over K). Every GEMV output row is now an independent simdgroup across all GPU cores, each weight read exactly once. Result: 49.0 vs 47.3 tok/s, +3.5% over gather_qmm. qwen1.5-moe-a2.7b is at parity (no regression); the small/fast MoE has less to gain. Still single-token decode, 4/8-bit affine only, off by default (MLXCEL_FUSED_MOE); MLXCEL_FUSED_MOE_SGY tunes simdgroups per threadgroup (default 8). The single-launch kernel and its diagnostic SG/R knobs are removed in favor of the two-kernel implementation. 6-bit (dots.llm1) is the next step.
This was referenced Jun 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step 2b of the fused decode-MoE kernel effort (#268). The 2a kernel (#275) was correctness-validated but ran one threadgroup per token (one GPU core), so it was ~88x slower than
gather_qmm. This PR parallelizes it into a kernel that beatsgather_qmmby ~3.5% on qwen3-30b-a3b, greedy output byte-identical.What changed
Two steps, each benched on qwen3-30b-a3b (M1 Ultra), greedy byte-identical throughout:
SIMD-cooperative GEMV (one simdgroup per output row, 32 lanes stride the contraction dim +
simd_sum): 0.54 → 21.6 tok/s (40x over 2a), but still 0.46x ofgather_qmm. An occupancy sweep (redundant tiling to force K·R threadgroups) confirmed the kernel is occupancy-bound, not tuning-bound — and that a fully-fused single launch can't escape it: the gate/up → down dependency runs through the per-expert activation, pinning each expert to one threadgroup.Two-kernel non-redundant split: stage the swiglu activation in global memory across two dispatches (A: gate/up + swiglu →
act_g[K, Dff]; B: down · score →partial[K, Din], summed over K). Every GEMV output row is now an independent simdgroup across all cores, each weight read exactly once: 49.0 vs 47.3 tok/s, +3.5%.Validation
gather_qmmpath on qwen3-30b-a3b (3 prompts) and qwen1.5-moe-a2.7b.cargo fmt --checkclean,cargo clippy --release --features metal,accelerateclean.Scope / flags
Single-token decode, 4/8-bit affine only, off by default (
MLXCEL_FUSED_MOE).MLXCEL_FUSED_MOE_SGYtunes simdgroups per threadgroup (default 8). The single-launch kernel and its diagnostic SG/R knobs are removed in favor of the two-kernel implementation.Next (not in this PR)
down_proj, the 0.51x target) and nemotron-h MoE benefit instead of falling back.MLXCEL_FUSED_MOEon by default.Refs #268.